package unity.messenger.security;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.math.BigInteger;
import java.security.InvalidKeyException;
import java.security.PublicKey;
import java.security.SignatureException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.util.Date;
import java.util.Random;

import javax.security.auth.x500.X500Principal;

import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.X509v1CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509v1CertificateBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;

import unity.messenger.security.exceptions.X509CertificateCreationException;

/**
 * Represents a X.509 Public Key Certificate
 * 
 * @author João Dias Amaro
 */
public class X509Certificate
{
	private java.security.cert.X509Certificate certificate;
	
	/**
	 * Creates a self-signed X509 V1 Public Key Certificate
	 * 
	 * @param info the DN (distinguished name)
	 * @param keypair the key pair containing the private and public key of the owner of this certificate
	 * @param serialNumber the serial number of this certificate. Setting this to <code>null</code> will generate a random serial number
	 * @param start the date from which this certificate is valid. If set to <code>null</code> the start date will be today's date and the validity will be 365 days
	 * @param end the date unit which this certificate is valid
	 * 
	 * @throws X509CertificateCreationException 
	 */
	public X509Certificate(String info, KeyPair keypair, BigInteger serialNumber, Date start, Date end) throws X509CertificateCreationException
	{
		if(info == null) throw new IllegalArgumentException("The info about this certificate owner must be supplied");
		if(keypair == null) throw new IllegalArgumentException("The key pair to create this certificate must be defined");
		if(serialNumber == null) serialNumber = new BigInteger("" + new Random().nextLong());
		if(start == null)
		{
			start = new Date();
			end = new Date(System.currentTimeMillis() + (1000L * 60 * 60 * 24 * 365));
		}
		else
		{
			if(end == null) throw new IllegalArgumentException("The start and end dates must be defined before creating the certificate");
		}
		
		X509v1CertificateBuilder v1CertGen = new JcaX509v1CertificateBuilder(new X500Principal(info), serialNumber, start, end, new X500Principal(info), keypair.getPublicKey());
		String signingAlgorithm;
		
		if(keypair.getAlgorithm().toString().startsWith(KeyPair.RSA_ALG_PREFIX))
		{
			signingAlgorithm = "SHA512WITHRSA";
		}
		else
		{
			int keysize = Integer.parseInt(keypair.getAlgorithm().toString().split("_")[1]);
			
			if(keysize < 224)
			{
				signingAlgorithm = "SHA1WITHECDSA";
			}
			else
			{
				signingAlgorithm = "SHA" + keysize + "WITHECDSA";
			}
		}
		
		try 
		{
			X509CertificateHolder cert = v1CertGen.build(new JcaContentSignerBuilder(signingAlgorithm).setProvider(new BouncyCastleProvider()).build(keypair.getPrivateKey()));
			this.certificate = new JcaX509CertificateConverter().setProvider(new BouncyCastleProvider()).getCertificate(cert);
		}
		catch(Exception e) 
		{
			throw new X509CertificateCreationException("It was not possible to build the Certificate", e);
		} 
	}
	
	/*
	 * Secondary Constructors
	 */
	
	public X509Certificate(InputStream certificateStream) throws X509CertificateCreationException
	{
		try 
		{
			CertificateFactory factory = CertificateFactory.getInstance("X.509", new BouncyCastleProvider());
			this.certificate = (java.security.cert.X509Certificate) factory.generateCertificate(certificateStream);
		}
		catch(CertificateException e) 
		{
			throw new X509CertificateCreationException("It was not possible to generate a X.509 Certificate", e);
		}
	}
	
	public X509Certificate(File certFile) throws X509CertificateCreationException, FileNotFoundException
	{
		this(new FileInputStream(certFile));
	}
	
	public X509Certificate(byte[] certBytes) throws X509CertificateCreationException
	{
		this(new ByteArrayInputStream(certBytes));
	}
	
	/*
	 * Validity Period Methods
	 */
	
	public Date getNotBeforeDate()
	{
		return this.certificate.getNotBefore();
	}
	
	public Date getNotAfterDate()
	{
		return this.certificate.getNotAfter();
	}
	
	public boolean isValidOn(Date date)
	{
		try 
		{
			this.certificate.checkValidity(date == null ? new Date() : date);
			return true;
		}
		catch(Exception e) 
		{
			return false;
		}
	}
	
	public boolean isValid()
	{
		return this.isValidOn(null);
	}
	
	/*
	 * Certificate Interface
	 */
	
	public PublicKey getPublicKey()
	{
		return this.certificate.getPublicKey();
	}
	
	public byte[] getEncoded() throws CertificateEncodingException
	{
		return this.certificate.getEncoded();
	}
	
	public String getIssuerInfo()
	{
		return this.certificate.getIssuerDN().toString();
	}
	
	public String getSubjectInfo()
	{
		return this.certificate.getSubjectDN().toString();
	}
	
	public BigInteger getSerialNumber()
	{
		return this.certificate.getSerialNumber();
	}
	
	public String getSignatureAlgorithm()
	{
		return this.certificate.getSigAlgName();
	}
	
	public boolean isIssuedBy(PublicKey issuerPK) throws SignatureException
	{
		try 
		{
			this.certificate.verify(issuerPK);
			return true;
		}
		catch(InvalidKeyException e) 
		{
			return false;
		}
		catch(Exception e) 
		{
			throw new SignatureException("It was not possible to check who issued the certificate", e);
		}
	}
	
	public boolean isSelfSigned() throws SignatureException
	{
		return this.isIssuedBy(this.getPublicKey());
	}
}
